/*
* Copyright (c) 2025 Shenzhen Kaihong Digital Industry Development Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

use std::{collections::BTreeSet, path::Path};

use anyhow::Context;
use askama::Template;
use convert_case::{Case, Casing};

use crate::msg_def::{BaseType, Field, MsgDef, MsgDefMap};

#[derive(Template)]
#[template(path = "message_types.h.askama")]
struct MessageTypesHTemplate<'a> {
    map: &'a MsgDefMap,
}

#[derive(Template)]
#[template(path = "messages.h.askama")]
struct MessagesHTemplate<'a> {
    map: &'a MsgDefMap,
}

#[derive(Template)]
#[template(path = "napi_msg_init.cpp.askama")]
struct NapiMsgInitCppTemplate<'a> {
    map: &'a MsgDefMap,
}

#[derive(Template)]
#[template(path = "napi_msg.h.askama", escape = "none")]
struct NapiMsgHTemplate<'a> {
    pkg_name: &'a str,
    msg_name: &'a str,
    msg_def: &'a MsgDef,
}

impl MsgDefMap {
    pub async fn gen_napi_project<P: AsRef<Path>>(&self, out_dir: P) -> anyhow::Result<()> {
        let out_dir = out_dir.as_ref();
        tokio::fs::create_dir_all(out_dir).await
            .context(format!("failed to create '{}'", out_dir.display()))?;

        let builtin_files = [
            ("MWServer.h", include_str!("../../../interfaces/c++/MWServer.h")),
            ("Node.h", include_str!("../../../interfaces/c++/Node.h")),
            ("BaseMessage.h", include_str!("../../../interfaces/napi/BaseMessage.h")),
            ("napi_utils.h", include_str!("../../../interfaces/napi/napi_utils.h")),
            ("Time.h", include_str!("../../../interfaces/napi/Time.h")),
            ("Duration.h", include_str!("../../../interfaces/napi/Duration.h")),
            ("napi_init.h", include_str!("../../../interfaces/napi/napi_init.h")),
            ("napi_init.cpp", include_str!("../../../interfaces/napi/napi_init.cpp")),
            ("napi_mw_server_init.cpp", include_str!("../../../interfaces/napi/napi_mw_server_init.cpp")),
            ("napi_node_init.cpp", include_str!("../../../interfaces/napi/napi_node_init.cpp")),
            ("CMakeLists.txt", include_str!("../../../interfaces/napi/CMakeLists.txt")),
            ("oh-package.json5", include_str!("../../../interfaces/napi/oh-package.json5")),
        ];

        for (path, content) in builtin_files {
            let file_path = out_dir.join(path);
            tokio::fs::write(&file_path, content).await
                .context(format!("failed to write NAPI builtin file to '{}'", file_path.display()))?;
        }

        let message_types_header_path = out_dir.join("message_types.h");
        let content = MessageTypesHTemplate { map: self }.render()
            .context("failed to generate NAPI message types header")?;
        tokio::fs::write(&message_types_header_path, content).await
            .context(format!("failed to write NAPI message types header to '{}'", message_types_header_path.display()))?;

        let messages_header_path = out_dir.join("messages.h");
        let content = MessagesHTemplate { map: self }.render()
            .context("failed to generate NAPI messages header")?;
        tokio::fs::write(&messages_header_path, content).await
            .context(format!("failed to write NAPI messages header to '{}'", messages_header_path.display()))?;

        let napi_msg_init_cpp_path = out_dir.join("napi_msg_init.cpp");
        let content = NapiMsgInitCppTemplate { map: self }.render()
            .context("failed to generate NAPI message init C++")?;
        tokio::fs::write(&napi_msg_init_cpp_path, content).await
            .context(format!("failed to write NAPI message init C++ to '{}'", napi_msg_init_cpp_path.display()))?;

        for (pkg_name, msg_defs) in self.map.iter() {
            let pkg_dir = out_dir.join(pkg_name);
            tokio::fs::create_dir_all(&pkg_dir).await
                .context(format!("failed to create '{}'", pkg_dir.display()))?;

            for (msg_name, msg_def) in msg_defs.iter() {
                let content = NapiMsgHTemplate { pkg_name, msg_name, msg_def }.render()
                    .context(format!("failed to generate NAPI header for '{}::{}'", pkg_name, msg_name))?;
                let msg_header_path = pkg_dir.join(format!("{}.h", msg_name.to_case(Case::Pascal)));
                tokio::fs::write(&msg_header_path, content).await
                    .context(format!("failed to write NAPI header to '{}'", msg_header_path.display()))?;
            }
        }
        Ok(())
    }

    fn napi_message_type_def_name(pkg_name: &str, msg_name: &str) -> String {
        format!("MESSAGE_{}_{}", pkg_name.to_case(Case::Constant), msg_name.to_case(Case::Constant))
    }

    fn napi_message_type_def_value(pkg_name: &str, msg_name: &str) -> String {
        format!("{}::{}", pkg_name, msg_name.to_case(Case::Pascal))
    }

    fn napi_msg_init_cpp_func_name(pkg_name: &str, msg_name: &str) -> String {
        format!("{}{}NapiInit", pkg_name.to_case(Case::Pascal), msg_name.to_case(Case::Pascal))
    }
}

impl MsgDef {
    fn collect_napi_msg_header_includes(&self) -> BTreeSet<String> {
        let mut includes = BTreeSet::from([
            "\"../napi_utils.h\"".to_string(),
            "\"../BaseMessage.h\"".to_string(),
            "\"framework/c++/bincode/Encoder.h\"".to_string(),
            "\"framework/c++/bincode/Decoder.h\"".to_string(),
            "<vector>".to_string(),
        ]);
        
        for field in &self.fields {
            let base_type = &field.field_type.base_type;
            match (base_type.pkg_name(), base_type.msg_name()) {
                (Some(pkg_name), msg_name) => {
                    includes.insert(format!("\"../{}/{}.h\"", pkg_name, msg_name.to_case(Case::Pascal)));
                },
                (None, msg_name) => {
                    match msg_name {
                        "time" => {
                            includes.insert("\"../Time.h\"".to_string());
                        },
                        "duration" => {
                            includes.insert("\"../Duration.h\"".to_string());
                        },
                        _ => {},
                    }
                },
            }
        }
        
        includes
    }
}

impl Field {
    fn napi_value_name(&self) -> String {
        format!("napi{}", self.field_name.to_case(Case::Pascal))
    }
}

impl<'a> NapiMsgHTemplate<'a> {
    fn type_name(base_type: &BaseType) -> String {
        match (base_type.pkg_name(), base_type.msg_name()) {
            (Some(pkg_name), msg_name) => {
                format!("{}::{}", pkg_name, msg_name.to_case(Case::Pascal))
            },
            (None, msg_name) => {
                match msg_name {
                    "time" => "OHMW_NAPI::Time".to_string(),
                    "duration" => "OHMW_NAPI::Duration".to_string(),
                    _ => msg_name.to_case(Case::Pascal),
                }
            },
        }
    }
}
